import java.util.HashMap;
import java.util.Map;

class EU4EncoderDecoder {
    private Map<Integer, Integer> cp1252_to_utf8_map;
    private Map<Integer, Integer> utf8_to_cp1252_map;

    public EU4EncoderDecoder() {
        cp1252_to_utf8_map = new HashMap<>();
        cp1252_to_utf8_map.put(0x80, 0x20AC);
        cp1252_to_utf8_map.put(0x82, 0x201A);
        cp1252_to_utf8_map.put(0x83, 0x0192);
        cp1252_to_utf8_map.put(0x84, 0x201E);
        cp1252_to_utf8_map.put(0x85, 0x2026);
        cp1252_to_utf8_map.put(0x86, 0x2020);
        cp1252_to_utf8_map.put(0x87, 0x2021);
        cp1252_to_utf8_map.put(0x88, 0x02C6);
        cp1252_to_utf8_map.put(0x89, 0x2030);
        cp1252_to_utf8_map.put(0x8A, 0x0160);
        cp1252_to_utf8_map.put(0x8B, 0x2039);
        cp1252_to_utf8_map.put(0x8C, 0x0152);
        cp1252_to_utf8_map.put(0x8E, 0x017D);
        cp1252_to_utf8_map.put(0x91, 0x2018);
        cp1252_to_utf8_map.put(0x92, 0x2019);
        cp1252_to_utf8_map.put(0x93, 0x201C);
        cp1252_to_utf8_map.put(0x94, 0x201D);
        cp1252_to_utf8_map.put(0x95, 0x2022);
        cp1252_to_utf8_map.put(0x96, 0x2013);
        cp1252_to_utf8_map.put(0x97, 0x2014);
        cp1252_to_utf8_map.put(0x98, 0x02DC);
        cp1252_to_utf8_map.put(0x99, 0x2122);
        cp1252_to_utf8_map.put(0x9A, 0x0161);
        cp1252_to_utf8_map.put(0x9B, 0x203A);
        cp1252_to_utf8_map.put(0x9C, 0x0153);
        cp1252_to_utf8_map.put(0x9E, 0x017E);
        cp1252_to_utf8_map.put(0x9F, 0x0178);

        utf8_to_cp1252_map = new HashMap<>();
        for (Map.Entry<Integer, Integer> entry : cp1252_to_utf8_map.entrySet()) {
            utf8_to_cp1252_map.put(entry.getValue(), entry.getKey());
        }
    }

    public int cp1252_to_utf8(int charCode) {
        return cp1252_to_utf8_map.getOrDefault(charCode, charCode);
    }

    public int utf8_to_cp1252(int charCode) {
        return utf8_to_cp1252_map.getOrDefault(charCode, charCode);
    }

    public String encode_char(String charStr, boolean to_utf8, boolean new_version) {
        int code_point = charStr.codePointAt(0);
        if (code_point < 256) {
            return charStr;
        }
        String hex_code = String.format("%04x", code_point);
        int low = Integer.parseInt(hex_code.substring(2), 16);
        int high = Integer.parseInt(hex_code.substring(0, 2), 16);

        int low_byte_offset = new_version? 14 : 15;
        int high_byte_offset = -9;

        int[] internal_chars = {
                0x00, 0x0A, 0x0D,
                0x20,
                0x22, 0x24,
                0x40, 0x5B, 0x5C,
                0x7B, 0x7D, 0x7E, 0x80,
                0xA3, 0xA4, 0xA7, 0xBD,
                0x3B,
                0x5D,
                0x5F,
                0x3D,
                0x23
        };

        if (to_utf8) {
            internal_chars = appendInt(internal_chars, 0x2F);
            if (!new_version) {
                internal_chars = removeInt(internal_chars, 0x20);
            }
        }

        int escape_chr = 0x10;
        for (int internal_char : internal_chars) {
            if (high == internal_char) {
                escape_chr += 2;
            }
        }
        for (int internal_char : internal_chars) {
            if (low == internal_char) {
                escape_chr += 1;
            }
        }

        if (escape_chr == 0x11) {
            low += low_byte_offset;
        } else if (escape_chr == 0x12) {
            high += high_byte_offset;
        } else if (escape_chr == 0x13) {
            low += low_byte_offset;
            high += high_byte_offset;
        }

        if (to_utf8) {
            low = cp1252_to_utf8(low);
            high = cp1252_to_utf8(high);
        }

        return new String(Character.toChars(escape_chr)) + new String(Character.toChars(low)) + new String(Character.toChars(high));
    }

    public String decode_char(String encoded_char, boolean to_utf8, boolean new_version) {
        if (encoded_char.length() == 1) {
            return encoded_char;
        }
        int escape_chr = encoded_char.codePointAt(0);
        int low = encoded_char.codePointAt(1);
        int high = encoded_char.codePointAt(2);

        if (to_utf8) {
            low = utf8_to_cp1252(low);
            high = utf8_to_cp1252(high);
        }

        int low_byte_offset = new_version? 14 : 15;
        int high_byte_offset = -9;

        if (escape_chr == 0x11) {
            low -= low_byte_offset;
        } else if (escape_chr == 0x12) {
            high -= high_byte_offset;
        } else if (escape_chr == 0x13) {
            low -= low_byte_offset;
            high -= high_byte_offset;
        }

        String hex_code = String.format("%02x%02x", high, low);
        int code_point = Integer.parseInt(hex_code, 16);
        return new String(Character.toChars(code_point));
    }

    public String encode_string(String s, boolean to_utf8, boolean new_version) {
        StringBuilder encoded = new StringBuilder();
        for (int i = 0; i < s.length(); i++) {
            String charStr = s.substring(i, i + 1);
            encoded.append(encode_char(charStr, to_utf8, new_version));
        }
        return encoded.toString();
    }

    public String decode_string(String encoded_str, boolean to_utf8, boolean new_version) {
        StringBuilder decoded = new StringBuilder();
        int i = 0;
        while (i < encoded_str.length()) {
            String charStr = encoded_str.substring(i, i + 1);
            int code_point = charStr.codePointAt(0);
            if (32 <= code_point && code_point <= 126) {
                decoded.append(charStr);
                i++;
            } else if (code_point < 0x10) {
                decoded.append(charStr);
                i++;
            } else {
                String encoded_char = encoded_str.substring(i, i + 3);
                decoded.append(decode_char(encoded_char, to_utf8, new_version));
                i += 3;
            }
        }
        return decoded.toString();
    }

    public String encode(String s) {
        return encode_string(s, true, true);
    }

    public String decode(String s) {
        return decode_string(s, true, true);
    }

    private int[] appendInt(int[] array, int value) {
        int[] newArray = new int[array.length + 1];
        System.arraycopy(array, 0, newArray, 0, array.length);
        newArray[array.length] = value;
        return newArray;
    }

    private int[] removeInt(int[] array, int value) {
        int index = -1;
        for (int i = 0; i < array.length; i++) {
            if (array[i] == value) {
                index = i;
                break;
            }
        }
        if (index == -1) {
            return array;
        }
        int[] newArray = new int[array.length - 1];
        System.arraycopy(array, 0, newArray, 0, index);
        System.arraycopy(array, index + 1, newArray, index, array.length - index - 1);
        return newArray;
    }
}

class EU4EncoderDecoderTest {
    public void test_encode() {
        System.out.println("加密测试");
        EU4EncoderDecoder encoder_decoder = new EU4EncoderDecoder();
        String originString = "留任 $MONARCH$";
        String encodeString = encoder_decoder.encode(originString);
        String decodeString = encoder_decoder.decode(encodeString);
        System.out.println("Original: " + originString + ", Encoded: " + encodeString + ", Decoded: " + decodeString);
    }

    public void test_decode() {
        System.out.println("解密测试");
        EU4EncoderDecoder encoder_decoder = new EU4EncoderDecoder();
        String encodeString = "$MONARCHTITLE$ $MONARCH$«ˆNg³R";
        String decodeString = encoder_decoder.decode(encodeString);
        System.out.println(decodeString);
    }

    public static void main(String[] args) {
        EU4EncoderDecoderTest test = new EU4EncoderDecoderTest();
        test.test_encode();
        test.test_decode();
    }
}
